<?php if (!defined('CORE_DIR')) die('Wrong entry point!');

/* 
 * Copyright 2010 Anton Muraviev a.k.a kuromaisu
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *     http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * 
 */

// =============================================================================
// Section: Database Driver
// =============================================================================

/* -----------------------------------------------------------------------------
 * Class: AeroDatabase
 * -----------------------------------------------------------------------------
 *
 * Base Database driver. All drivers should extend this class implement its
 * placeholder methods.
 *
 */
abstract class AeroDatabase
{
	// Object: connection
	//
	// Database connection resource.
	protected $connection;
	
	// String: database
	//
	// Database name this driver should use.
	protected $database;
		
	// String: selected_database
	//
	// Name of the database selected by the last used driver.
	protected static $selected_database;
	
	// Array: statistics
	//
	// Publicly available statistics for all performed queries.
	// 
	// Example:
	// (begin example)
	// array(
	// 	array(
	// 		'query' => "SELECT 1",
	// 		'time'	=> 0.0000001,
	// 		'connection' => 'default'
	// 	),
	// 	...
	// )
	// (end)
	public static $statistics = array();
	
	// Array: drivers
	//
	// All DB driver instances are stored here.
	private static $drivers = array();
	
	// Variables: Escape characters
	//
	// quote_open	-	Character(s) to prepend table and column names with in generated queries.
	// quote_close	-	Character(s) to append to table and column names.
	protected $quote_open = '';
	protected $quote_close = '';

	/*
	 * Method: connect
	 * 
	 * Creates a database driver using the configuration settings set by <Aero::setOption>.
	 * 
	 * Example:
	 * (begin example)
	 * // First we need to set connection parameters (this can be done in a separate file)
	 * Aero::setOption('database.my_database',array(
	 * 	'driver' => 'db/mysql/driver',
	 * 	'server' => 'localhost',
	 * 	'username' => 'user',
	 * 	'password' => 'pass',
	 * 	'database' => 'some_db'
	 * ));
	 * 
	 * // Now we can load the driver.
	 * $db = AeroDatabase::connect('my_database');
	 * (end)
	 *
	 * Parameters:
	 * 	string	-	database name.
	 * 
	 * Throws:
	 * 	ErrorException	-	if parameters included unsupported driver.
	 */
	public static function connect($db = 'default')
	{
		if (isset(self::$drivers[$db]))
		{
			return self::$drivers[$db];
		}
		
		// TODO: use plural "databases" namespace instead.
		$driver = Aero::option('database.' . $db . '.driver', false);
		
		if (empty($driver))
		{
			throw new ErrorException('<var>' . $db . '</var> database has no driver set.', 0, E_USER_ERROR);
		}
		
		return self::$drivers[$db] = Aero::load($driver, $db);
	}
	
	/*
	 * Method: logQueryTime
	 * 
	 * Parameters:
	 * 	string	-	query performed;
	 * 	float	-	time it took;
	 * 	string	-	connection identifier.
	 * 
	 * See also:
	 *  <statistics>
	 */
	public static function logQueryTime($sql, $time, $connection = '')
	{
		// TODO: Add configuration options check to prevent query time logging altogether
		// or keep track of the last few queries.
		AeroDatabase::$statistics[] = array(
			'query' => $sql,
			'time' => $time,
			'connection' => $connection
		);
	}
	
	/*
	 * Constructor: __construct
	 * 
	 * Default constructor implementation.
	 * 
	 * Parameters:
	 * 	string	-	database name.
	 */
	public function __construct($database = '')
	{
		if (empty($database))
		{
			$database = 'default';
		}
		
		if (is_string($database))
		{
			$database = Aero::option('database.' . $database, array(
				'server' => '',
				'username' => '',
				'password' => '',
				'database' => $database
			));
		}
		
		$this->_connect($database);
		$this->_select($database['database']);
		
		$this->database = $database['database'];
	}
	
	/*
	 * Method: query
	 *
	 * Performs a database query using SQL statement template and its parameters.
	 * 
	 * Examples:
	 * (begin example)
	 * // Conditional SELECT
	 * $db->query('SELECT * FROM books WHERE genre = :genre: AND in_stock = :in_stock:',array(
	 * 	'genre' => 'fiction',
	 * 	'in_stock' => true
	 * ));
	 * 
	 * // Simple insert
	 * $db->query('INSERT INTO books (:keys:) VALUES (:values:)', null, array(
	 * 	'title' => "Don't Make Me Think",
	 * 	'year'	=> 2002,
	 * 	'author_id' => 19,
	 * 	'in_stock' => true
	 * ));
	 * 
	 * $id = $db->insertId();
	 * 
	 * // Condition UPDATE. Note that we are not using an array for the second parameter.
	 * // It will however be converted to an array(0 => $id), hence we can use :0: token.
	 * $db->query('UPDATE books SET :keys_values: WHERE id = :0:', $id, array(
	 * 	'in_stock' => false
	 * ));
	 * (end)
	 * 
	 * Parameters:
	 * 	string	-	SQL statement (template);
	 * 	array	-	template variables;
	 * 	array	-	SQL values used by INSERT and UPDATE queries.
	 * 
	 * Returns:
	 * 	AeroDatabaseResult	-	for SELECT-like queries;
	 * 	true				-	for successful queries of different kind;
	 * 	false				-	otherwise.
	 * 
	 * See also:
	 * 	<escape>, <escapeString>
	 */
	public function query($sql, $vars = array(), $values = array())
	{
		if (self::$selected_database !== $this->database)
		{
			$this->__select($this->database);
		}
		
		if (!is_array($vars))
		{
			$vars = array($vars);
		}
			
		if (!is_array($values))
		{
			throw new ErrorException('<code>'.get_class($this).'::query()</code>\'s 3<sup>rd</sup> parameter should be of type Array.', 0, E_USER_ERROR);
		}
		
		// If no variables has been passes, query immediately.
		if (count($vars) == 0 && count($values) == 0)
		{
			return $this->_query($sql);
		}
		
		// Replace tokens with excaped values.
		$_tokens = $_values = array();
		
		foreach ($vars as $key => $value)
		{
			$i = $this->protect($key);
			// Escape the colon character for safety reasons
			$_values[$i] = str_replace(array('{','}'),array('{-:left:-}','{-:right:-}'),$this->escape($value));
			$_tokens[$i] = '{' . $key . '}';
		}
		
		$sql = str_replace($_tokens,$_values,$sql);
		
		// Replace special tokens ({keys}, {values} and {keys=values})
		if (count($values) > 0)
		{
			$_values = array();
			
			foreach ($values as $key => $value)
			{
				$_values[$this->protect($key)] = $this->escape($value);
			}
			
			if (strpos($sql,'{keys=values}') !== FALSE)
			{
				$keys_values = array();

				foreach ($_values as $key => $value)
				{
					$keys_values[] = $key . ' = ' . $value;
				}

				$sql = str_replace('{keys=values}',
					implode(', ',$keys_values),
					$sql);
			}

			if (strpos($sql,'{keys}') !== FALSE)
			{
				$sql = str_replace('{keys}',
					implode(', ',array_keys($_values)),
					$sql);
			}

			if (strpos($sql,'{values}') !== FALSE)
			{
				$sql = str_replace('{values}',
					implode(', ', $_values),
					$sql);
			}
		}
		
		// It is safe to restore the colon now
		$sql = str_replace(array('{-:left:-}','{-:right:-}'),array('{','}'),$sql);
		
		return $this->_query($sql);
	}
	
	/*
	 * Method: escape
	 *
	 * Escapes value to be used safely in a query.
	 * 
	 * Arrays are serialized, while objects are being cast to strings.
	 * NULL becomes 'NULL'. TRUE and FALSE become "1" and "0" respectively.
	 * 
	 * Parameters:
	 * 	misc	-	any value.
	 * 
	 * Returns:
	 * 	string	-	escaped value.
	 * 
	 * See also:
	 * 	<escapeString>
	 */
	public function escape($value)
	{
		switch (gettype($value))
		{
			case 'array':
				$value = serialize($value); // and then escape as string
			case 'object':
				$value = (string) $value; // implicitly cast to string
			case 'string': 
				$value = '"' . $this->escapeString($value) . '"';
				break;
			case 'boolean':
				$value = $value ? '1' : '0';
				break;
			case 'NULL':
				$value = 'NULL';
		}
		
		return $value;
	}
	
	/*
	 * Method: protect
	 *
	 * Protects table and column names or specifiers.
	 * 
	 * Parameters:
	 * 	string	-	a table or column name.
	 * 
	 * Returns:
	 * 	string	-	a safe to use specifier.
	 */
	public function protect($string)
	{
		return preg_replace('~\s*\.\s*~', $this->quote_close.'.'.$this->quote_open,
			$this->quote_open.trim($string).$this->quote_close);
	}
	
	/*
	 * Method: insertId
	 *
	 * Returns id of the newly created row.
	 * 
	 * _To_be_implemented_by_children_classes_.
	 */
	abstract public function insertId();
	
	/*
	 * Method: affectedRows
	 *
	 * Returns number of affected rows.
	 * 
	 * _To_be_implemented_by_children_classes_.
	 */
	abstract public function affectedRows();
	
	/*
	 * Method: escapeString
	 * 
	 * Escapes a string to be used in a query.
	 * 
	 * _To_be_implemented_by_children_classes_.
	 * 
	 * See also:
	 * 	<escape>
	 */
	abstract public function escapeString($string);
	
	/*
	 * Method: _query
	 * 
	 * Performs an actual SQL query.
	 * 
	 * _To_be_implemented_by_children_classes_.
	 */
	abstract protected function _query($query);
	
	/*
	 * Method: _select
	 *
	 * Selects database to query.
	 * 
	 * _To_be_implemented_by_children_classes_.
	 * 
	 * Parameters:
	 * 	string	-	database name.
	 * 
	 * See also:
	 * 	<connect>
	 */
	abstract protected function _select($name);
	
	/*
	 * Method: _connect
	 *
	 * Establishes database connection and sets <$connection> property.
	 * 
	 * _To_be_implemented_by_children_classes_.
	 * 
	 * Parameters:
	 * 	array	-	connection parameters.
	 * 
	 * See also:
	 * 	<connect>
	 */
	abstract protected function _connect($params);
}
